/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2008-2011, Salzburg Research. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * - Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 * - Neither the name of the KiWi Project nor the names of its contributors
 * may be used to endorse or promote products derived from this software
 * without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * Contributor(s):
 * Stefan
 *
 * File Info:
 * User: Stefan
 * Date: 2011/02/01
 * Time: 17:04
 *
 * Project: KiWi2, kiwi-core module
 */

package kiwi.core.services.io;

import info.aduna.net.ParsedURI;
import info.aduna.xml.XMLReaderFactory;
import info.aduna.xml.XMLUtil;
import org.openrdf.model.*;
import org.openrdf.model.impl.URIImpl;
import org.openrdf.model.impl.ValueFactoryImpl;
import org.openrdf.model.vocabulary.RDF;
import org.openrdf.rio.ParseLocationListener;
import org.openrdf.rio.RDFFormat;
import org.openrdf.rio.RDFHandlerException;
import org.openrdf.rio.RDFParseException;
import org.openrdf.rio.helpers.RDFParserBase;
import org.xml.sax.*;

import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.*;


/**
 * A reader for RDF-Quads file. It is similar to the RDFXML Syntax. Base for this realisation is the RDFa Quads
 * extension. New is the graph:graph attribute which describe a Context. It is not complete backward compatible
 * to RDFXML. This class describe the Reader which interpretate the file.
 * Example of RDF-Quads:
 *
 * <rdf:Description rdf:about="http://www.w3.org/TR/rdf-syntax-grammar">
 * <ex:editor graph:graph="http://example.org/graph1">
 *   <rdf:Description>
 *     <ex:homePage rdf:resource="http://purl.org/net/dajobe/"/>
 *     <ex:fullName>Dave Beckett</ex:fullName>
 *   </rdf:Description>
 * </ex:editor>
 * <dc:title graph:graph="http://example.org/graph2">RDF/XML Syntax Specification (Revised)</dc:title>
 *</rdf:Description>
 *
 * User: srobert
 */
public class RdfQuadsReader extends RDFParserBase {

    /*-----------*
      * Variables *
      *-----------*/

    /**
     * A filter filtering calls to SAX methods specifically for this parser.
     */
    private SAXFilter saxFilter;

    /**
     * The base URI of the document. This variable is set when
     * <tt>parse(inputStream, baseURI)</tt> is called and will not be changed
     * during parsing.
     */
    private String documentURI;

    /**
     * The language of literal values as can be specified using xml:lang
     * attributes. This variable is set/modified by the SAXFilter during parsing
     * such that it always represents the language of the context in which
     * elements are reported.
     */
    private String xmlLang;

    /**
     * Named Graph (Knowledgespaces) support for RDFXML
     * It is to use like xml:lang
     *
     * You can set a graph definition by a subject/object or by a predicate
     */
    private String context;

    /**
     * A stack of node- and property elements.
     */
    private Stack<Object> elementStack = new Stack<Object>();

    /**
     * A set containing URIs that have been generated as a result of rdf:ID
     * attributes. These URIs should be unique within a single document.
     */
    private Set<URI> usedIDs = new HashSet<URI>();


    /**
     * Add of Stefan;
     * The constant URI of the graph
     */
    private final static String GRAPH_EXTENSION_NAMESPACE = "http://www.kiwi-community.eu/rdfquads#";

    /*--------------*
      * Constructors *
      *--------------*/

    /**
     * Creates a new RDFXMLParser that will use a {@link ValueFactoryImpl} to
     * create RDF model objects.
     */
    public RdfQuadsReader() {
        super();

        // SAXFilter does some filtering and verifying of SAX events
        saxFilter = new SAXFilter(this);
    }

    /**
     * Creates a new RDFXMLParser that will use the supplied
     * <tt>ValueFactory</tt> to create RDF model objects.
     *
     * @param valueFactory A ValueFactory.
     */
    public RdfQuadsReader(ValueFactory valueFactory) {
        super(valueFactory);

        // SAXFilter does some filtering and verifying of SAX events
        saxFilter = new SAXFilter(this);
    }

    /*---------*
      * Methods *
      *---------*/

    // implements RDFParser.getRDFFormat()
    public final RDFFormat getRDFFormat() {
        return RDFFormat.RDFXML;
    }

    /**
     * Sets the parser in a mode to parse stand-alone RDF documents. In
     * stand-alone RDF documents, the enclosing <tt>rdf:RDF</tt> root element is
     * optional if this root element contains just one element (e.g.
     * <tt>rdf:Description</tt>.
     */
    public void setParseStandAloneDocuments(boolean standAloneDocs) {
        saxFilter.setParseStandAloneDocuments(standAloneDocs);
    }

    /**
     * Returns whether the parser is currently in a mode to parse stand-alone RDF
     * documents.
     *
     * @see #setParseStandAloneDocuments
     */
    public boolean getParseStandAloneDocuments() {
        return saxFilter.getParseStandAloneDocuments();
    }

    /**
     * Parses the data from the supplied InputStream, using the supplied baseURI
     * to resolve any relative URI references.
     *
     * @param in      The InputStream from which to read the data, must not be
     *                <tt>null</tt>.
     * @param baseURI The URI associated with the data in the InputStream, must not be
     *                <tt>null</tt>.
     * @throws IOException              If an I/O error occurred while data was read from the InputStream.
     * @throws RDFParseException        If the parser has found an unrecoverable parse error.
     * @throws RDFHandlerException      If the configured statement handler encountered an unrecoverable
     *                                  error.
     * @throws IllegalArgumentException If the supplied input stream or base URI is <tt>null</tt>.
     */
    public synchronized void parse(InputStream in, String baseURI)
            throws IOException, RDFParseException, RDFHandlerException {
        if (in == null) {
            throw new IllegalArgumentException("Input stream cannot be 'null'");
        }
        if (baseURI == null) {
            throw new IllegalArgumentException("Base URI cannot be 'null'");
        }

        InputSource inputSource = new InputSource(in);
        inputSource.setSystemId(baseURI);

        parse(inputSource);
    }

    /**
     * Parses the data from the supplied Reader, using the supplied baseURI to
     * resolve any relative URI references.
     *
     * @param reader  The Reader from which to read the data, must not be <tt>null</tt>.
     * @param baseURI The URI associated with the data in the InputStream, must not be
     *                <tt>null</tt>.
     * @throws IOException              If an I/O error occurred while data was read from the InputStream.
     * @throws RDFParseException        If the parser has found an unrecoverable parse error.
     * @throws RDFHandlerException      If the configured statement handler has encountered an
     *                                  unrecoverable error.
     * @throws IllegalArgumentException If the supplied reader or base URI is <tt>null</tt>.
     */
    public synchronized void parse(Reader reader, String baseURI)
            throws IOException, RDFParseException, RDFHandlerException {
        if (reader == null) {
            throw new IllegalArgumentException("Reader cannot be 'null'");
        }
        if (baseURI == null) {
            throw new IllegalArgumentException("Base URI cannot be 'null'");
        }

        InputSource inputSource = new InputSource(reader);
        inputSource.setSystemId(baseURI);

        parse(inputSource);
    }

    private void parse(InputSource inputSource)
            throws IOException, RDFParseException, RDFHandlerException {
        try {
            documentURI = inputSource.getSystemId();

            // saxFilter.clear();
            saxFilter.setDocumentURI(documentURI);

            XMLReader xmlReader = XMLReaderFactory.createXMLReader();
            xmlReader.setContentHandler(saxFilter);

            rdfHandler.startRDF();
            xmlReader.parse(inputSource);
            rdfHandler.endRDF();
        } catch (SAXParseException e) {
            Exception wrappedExc = e.getException();
            if (wrappedExc == null) {
                wrappedExc = e;
            }
            reportFatalError(wrappedExc, e.getLineNumber(), e.getColumnNumber());
        } catch (SAXException e) {
            Exception wrappedExc = e.getException();
            if (wrappedExc == null) {
                wrappedExc = e;
            }

            if (wrappedExc instanceof RDFParseException) {
                throw (RDFParseException) wrappedExc;
            } else if (wrappedExc instanceof RDFHandlerException) {
                throw (RDFHandlerException) wrappedExc;
            } else {
                reportFatalError(wrappedExc);
            }
        } finally {
            // Clean up
            saxFilter.clear();
            xmlLang = null;
            this.context = null;
            elementStack.clear();
            usedIDs.clear();
            clear();
        }
    }

    /*-----------------------------*
      * Methods called by SAXFilter *
      *-----------------------------*/

    @Override
    protected void setBaseURI(ParsedURI baseURI) {
        // Note: we need to override this method to allow SAXFilter to access it
        super.setBaseURI(baseURI);
    }

    void setXMLLang(String xmlLang) {
        if ("".equals(xmlLang)) {
            this.xmlLang = null;
        } else {
            this.xmlLang = xmlLang;
        }
    }

    void setContext(String context) {
        if ("".equals(context)) {
            this.context = null;
        } else {
            this.context = context;
        }
    }

    void startElement(String namespaceURI, String localName, String qName, Atts atts)
            throws RDFParseException, RDFHandlerException {
        if (topIsProperty()) {
            // this element represents the subject and/or object of a statement
            processNodeElt(namespaceURI, localName, qName, atts, false);
        } else {
            // this element represents a property
            processPropertyElt(namespaceURI, localName, qName, atts, false);
        }
    }

    void endElement(String namespaceURI, String localName, String qName)
            throws RDFParseException, RDFHandlerException {
        Object topElement = peekStack(0);

        if (topElement instanceof NodeElement) {
            // Check if top node is 'volatile', meaning that it doesn't have a
            // start- and end element associated with it.
            if (((NodeElement) topElement).isVolatile()) {
                elementStack.pop();
            }
        } else {
            // topElement instanceof PropertyElement
            PropertyElement predicate = (PropertyElement) topElement;

            if (predicate.parseCollection()) {
                Resource lastListResource = predicate.getLastListResource();

                if (lastListResource == null) {
                    // no last list resource, list must have been empty.
                    NodeElement subject = (NodeElement) peekStack(1);

                    reportStatement(subject.getResource(), predicate.getURI(), RDF.NIL);

                    handleReification(RDF.NIL);
                } else {
                    // Generate the final tail of the list.
                    reportStatement(lastListResource, RDF.REST, RDF.NIL);
                }
            }

        }

        elementStack.pop();
    }

    void emptyElement(String namespaceURI, String localName, String qName, Atts atts)
            throws RDFParseException, RDFHandlerException {
        if (topIsProperty()) {
            // this element represents the subject and/or object of a statement
            processNodeElt(namespaceURI, localName, qName, atts, true);
        } else {
            // this element represents a property
            processPropertyElt(namespaceURI, localName, qName, atts, true);
        }
    }

    void text(String text)
            throws RDFParseException, RDFHandlerException {
        if (!topIsProperty()) {
            reportError("unexpected literal");
            return;
        }

        PropertyElement propEl = (PropertyElement) peekStack(0);
        URI datatype = propEl.getDatatype();

        Literal lit = createLiteral(text, xmlLang, datatype);

        NodeElement subject = (NodeElement) peekStack(1);
        PropertyElement predicate = (PropertyElement) peekStack(0);

        reportStatement(subject.getResource(), predicate.getURI(), lit);

        handleReification(lit);
    }

    /*------------------------*
      * RDF processing methods *
      *------------------------*/

    /* Process a node element (can be both subject and object) */
    private void processNodeElt(String namespaceURI, String localName, String qName, Atts atts,
                                boolean isEmptyElt)
            throws RDFParseException, RDFHandlerException {
        if (verifyData()) {
            // Check the element name
            checkNodeEltName(namespaceURI, localName, qName);
        }

        Resource nodeResource = getNodeResource(atts);
        NodeElement nodeElement = new NodeElement(nodeResource);

        if (!elementStack.isEmpty()) {
            // node can be object of a statement, or part of an rdf:List
            NodeElement subject = (NodeElement) peekStack(1);
            PropertyElement predicate = (PropertyElement) peekStack(0);

            if (predicate.parseCollection()) {
                Resource lastListRes = predicate.getLastListResource();
                BNode newListRes = createBNode();

                if (lastListRes == null) {
                    // first element in the list
                    reportStatement(subject.getResource(), predicate.getURI(), newListRes);

                    handleReification(newListRes);
                } else {
                    // not the first element in the list
                    reportStatement(lastListRes, RDF.REST, newListRes);
                }

                reportStatement(newListRes, RDF.FIRST, nodeResource);

                predicate.setLastListResource(newListRes);
            } else {
                reportStatement(subject.getResource(), predicate.getURI(), nodeResource);

                handleReification(nodeResource);
            }
        }

        if (!localName.equals("Description") || !namespaceURI.equals(RDF.NAMESPACE)) {
            // element name is uri's type
            URI className = null;
            if ("".equals(namespaceURI)) {
                // No namespace, use base URI
                className = buildResourceFromLocalName(localName);
            } else {
                className = createURI(namespaceURI + localName);
            }
            reportStatement(nodeResource, RDF.TYPE, className);
        }

        Att type = atts.removeAtt(RDF.NAMESPACE, "type");
        if (type != null) {
            // rdf:type attribute, value is a URI-reference
            URI className = resolveURI(type.getValue());

            reportStatement(nodeResource, RDF.TYPE, className);
        }

        if (verifyData()) {
            checkRDFAtts(atts);
        }

        processSubjectAtts(nodeElement, atts);

        if (!isEmptyElt) {
            elementStack.push(nodeElement);
        }
    }

    /**
     * Retrieves the resource of a node element (subject or object) using
     * relevant attributes (rdf:ID, rdf:about and rdf:nodeID) from its attributes
     * list.
     *
     * @return a resource or a bNode.
     */
    private Resource getNodeResource(Atts atts)
            throws RDFParseException {
        Att id = atts.removeAtt(RDF.NAMESPACE, "ID");
        Att about = atts.removeAtt(RDF.NAMESPACE, "about");
        Att nodeID = atts.removeAtt(RDF.NAMESPACE, "nodeID");

        if (verifyData()) {
            int definedAttsCount = 0;

            if (id != null) {
                definedAttsCount++;
            }
            if (about != null) {
                definedAttsCount++;
            }
            if (nodeID != null) {
                definedAttsCount++;
            }

            if (definedAttsCount > 1) {
                reportError("Only one of the attributes rdf:ID, rdf:about or rdf:nodeID can be used here");
            }
        }

        Resource result = null;

        if (id != null) {
            result = buildURIFromID(id.getValue());
        } else if (about != null) {
            result = resolveURI(about.getValue());
        } else if (nodeID != null) {
            result = createBNode(nodeID.getValue());
        } else {
            // No resource specified, generate a bNode
            result = createBNode();
        }

        return result;
    }

    /**
     * processes subject attributes.
     */
    private void processSubjectAtts(NodeElement nodeElt, Atts atts)
            throws RDFParseException, RDFHandlerException {
        Resource subject = nodeElt.getResource();

        Iterator<Att> iter = atts.iterator();

        while (iter.hasNext()) {
            Att att = iter.next();

            URI predicate = createURI(att.getURI());
            Literal lit = createLiteral(att.getValue(), xmlLang, null);

            reportStatement(subject, predicate, lit);
        }
    }

    private void processPropertyElt(String namespaceURI, String localName, String qName, Atts atts,
                                    boolean isEmptyElt)
            throws RDFParseException, RDFHandlerException {
        if (verifyData()) {
            checkPropertyEltName(namespaceURI, localName, qName);
        }

        // Get the URI of the property
        URI propURI = null;
        if (namespaceURI.equals("")) {
            // no namespace URI
            reportError("unqualified property element <" + qName + "> not allowed");
            // Use base URI as namespace:
            propURI = buildResourceFromLocalName(localName);
        } else {
            propURI = createURI(namespaceURI + localName);
        }

        // List expansion rule
        if (propURI.equals(RDF.LI)) {
            NodeElement subject = (NodeElement) peekStack(0);
            propURI = createURI(RDF.NAMESPACE + "_" + subject.getNextLiCounter());
        }

        // Push the property on the stack.
        PropertyElement predicate = new PropertyElement(propURI);
        elementStack.push(predicate);

        // Check if property has a reification ID
        Att id = atts.removeAtt(RDF.NAMESPACE, "ID");
        if (id != null) {
            URI reifURI = buildURIFromID(id.getValue());
            predicate.setReificationURI(reifURI);
        }

        // Check for presence of rdf:parseType attribute
        Att parseType = atts.removeAtt(RDF.NAMESPACE, "parseType");

        if (parseType != null) {
            if (verifyData()) {
                checkNoMoreAtts(atts);
            }

            String parseTypeValue = parseType.getValue();

            if (parseTypeValue.equals("Resource")) {
                BNode objectResource = createBNode();
                NodeElement subject = (NodeElement) peekStack(1);

                reportStatement(subject.getResource(), propURI, objectResource);

                if (isEmptyElt) {
                    handleReification(objectResource);
                } else {
                    NodeElement object = new NodeElement(objectResource);
                    object.setIsVolatile(true);
                    elementStack.push(object);
                }
            } else if (parseTypeValue.equals("Collection")) {
                if (isEmptyElt) {
                    NodeElement subject = (NodeElement) peekStack(1);
                    reportStatement(subject.getResource(), propURI, RDF.NIL);
                    handleReification(RDF.NIL);
                } else {
                    predicate.setParseCollection(true);
                }
            } else {
                // other parseType
                if (!parseTypeValue.equals("Literal")) {
                    reportWarning("unknown parseType: " + parseType.getValue());
                }

                if (isEmptyElt) {
                    NodeElement subject = (NodeElement) peekStack(1);

                    Literal lit = createLiteral("", null, RDF.XMLLITERAL);

                    reportStatement(subject.getResource(), propURI, lit);

                    handleReification(lit);
                } else {
                    // The next string is an rdf:XMLLiteral
                    predicate.setDatatype(RDF.XMLLITERAL);

                    saxFilter.setParseLiteralMode();
                }
            }
        }
        // parseType == null
        else if (isEmptyElt) {
            // empty element without an rdf:parseType attribute

            // Note: we handle rdf:datatype attributes here to allow datatyped
            // empty strings in documents. The current spec does have a
            // production rule that matches this, which is likely to be an
            // omission on its part.
            Att datatype = atts.getAtt(RDF.NAMESPACE, "datatype");

            if (atts.size() == 0 || atts.size() == 1 && datatype != null) {
                // element had no attributes, or only the optional
                // rdf:ID and/or rdf:datatype attributes.
                NodeElement subject = (NodeElement) peekStack(1);

                URI dtURI = null;
                if (datatype != null) {
                    dtURI = createURI(datatype.getValue());
                }

                Literal lit = createLiteral("", xmlLang, dtURI);

                reportStatement(subject.getResource(), propURI, lit);
                handleReification(lit);
            } else {
                // Create resource for the statement's object.
                Resource resourceRes = getPropertyResource(atts);

                // All special rdf attributes have been checked/removed.
                if (verifyData()) {
                    checkRDFAtts(atts);
                }

                NodeElement resourceElt = new NodeElement(resourceRes);
                NodeElement subject = (NodeElement) peekStack(1);

                reportStatement(subject.getResource(), propURI, resourceRes);
                handleReification(resourceRes);

                Att type = atts.removeAtt(RDF.NAMESPACE, "type");
                if (type != null) {
                    // rdf:type attribute, value is a URI-reference
                    URI className = resolveURI(type.getValue());

                    reportStatement(resourceRes, RDF.TYPE, className);
                }

                processSubjectAtts(resourceElt, atts);
            }
        } else {
            // Not an empty element, sub elements will follow.

            // Check for rdf:datatype attribute
            Att datatype = atts.removeAtt(RDF.NAMESPACE, "datatype");
            if (datatype != null) {
                URI dtURI = resolveURI(datatype.getValue());
                predicate.setDatatype(dtURI);
            }

            // No more attributes are expected.
            if (verifyData()) {
                checkNoMoreAtts(atts);
            }
        }

        if (isEmptyElt) {
            // Empty element has been pushed on the stack
            // at the start of this method, remove it.
            elementStack.pop();
        }
    }

    /**
     * Retrieves the object resource of a property element using relevant
     * attributes (rdf:resource and rdf:nodeID) from its attributes list.
     *
     * @return a resource or a bNode.
     */
    private Resource getPropertyResource(Atts atts)
            throws RDFParseException {
        Att resource = atts.removeAtt(RDF.NAMESPACE, "resource");
        Att nodeID = atts.removeAtt(RDF.NAMESPACE, "nodeID");

        if (verifyData()) {
            int definedAttsCount = 0;

            if (resource != null) {
                definedAttsCount++;
            }
            if (nodeID != null) {
                definedAttsCount++;
            }

            if (definedAttsCount > 1) {
                reportError("Only one of the attributes rdf:resource or rdf:nodeID can be used here");
            }
        }

        Resource result = null;

        if (resource != null) {
            result = resolveURI(resource.getValue());
        } else if (nodeID != null) {
            result = createBNode(nodeID.getValue());
        } else {
            // No resource specified, generate a bNode
            result = createBNode();
        }

        return result;
    }

    /*
      * Processes any rdf:ID attributes that generate reified statements. This
      * method assumes that a PropertyElement (which can have an rdf:ID attribute)
      * is on top of the stack, and a NodeElement is below that.
      */
    private void handleReification(Value value)
            throws RDFParseException, RDFHandlerException {
        PropertyElement predicate = (PropertyElement) peekStack(0);

        if (predicate.isReified()) {
            NodeElement subject = (NodeElement) peekStack(1);
            URI reifRes = predicate.getReificationURI();
            reifyStatement(reifRes, subject.getResource(), predicate.getURI(), value);
        }
    }

    private void reifyStatement(Resource reifNode, Resource subj, URI pred, Value obj)
            throws RDFParseException, RDFHandlerException {
        reportStatement(reifNode, RDF.TYPE, RDF.STATEMENT);
        reportStatement(reifNode, RDF.SUBJECT, subj);
        reportStatement(reifNode, RDF.PREDICATE, pred);
        reportStatement(reifNode, RDF.OBJECT, obj);
    }

    /**
     * Builds a Resource from a non-qualified localname.
     */
    private URI buildResourceFromLocalName(String localName)
            throws RDFParseException {
        return resolveURI("#" + localName);
    }

    /**
     * Builds a Resource from the value of an rdf:ID attribute.
     */
    private URI buildURIFromID(String id)
            throws RDFParseException {
        if (verifyData()) {
            // Check if 'id' is a legal NCName
            if (!XMLUtil.isNCName(id)) {
                reportError("Not an XML Name: " + id);
            }
        }

        URI uri = resolveURI("#" + id);

        if (verifyData()) {
            // ID (URI) should be unique in the current document

            if (!usedIDs.add(uri)) {
                // URI was not added because the set already contained an equal
                // string
                reportError("ID '" + id + "' has already been defined");
            }
        }

        return uri;
    }

    // Overrides RDFParserBase._createBNode(...)
    protected BNode createBNode(String nodeID)
            throws RDFParseException {
        if (verifyData()) {
            // Check if 'nodeID' is a legal NCName
            if (!XMLUtil.isNCName(nodeID)) {
                reportError("Not an XML Name: " + nodeID);
            }
        }

        return super.createBNode(nodeID);
    }

    private Object peekStack(int distFromTop) {
        return elementStack.get(elementStack.size() - 1 - distFromTop);
    }

    private boolean topIsProperty() {
        return elementStack.isEmpty() || peekStack(0) instanceof PropertyElement;
    }

    /**
     * Checks whether the node element name is from the RDF namespace and, if so,
     * if it is allowed to be used in a node element. If the name is equal to one
     * of the disallowed names (RDF, ID, about, parseType, resource, nodeID,
     * datatype and li), an error is generated. If the name is not defined in the
     * RDF namespace, but it claims that it is from this namespace, a warning is
     * generated.
     */
    private void checkNodeEltName(String namespaceURI, String localName, String qName)
            throws RDFParseException {
        if (RDF.NAMESPACE.equals(namespaceURI)) {

            if (localName.equals("Description") || localName.equals("Seq") || localName.equals("Bag")
                    || localName.equals("Alt") || localName.equals("Statement") || localName.equals("Property")
                    || localName.equals("List") || localName.equals("subject") || localName.equals("predicate")
                    || localName.equals("object") || localName.equals("type") || localName.equals("value")
                    || localName.equals("first") || localName.equals("rest") || localName.equals("nil")
                    || localName.startsWith("_")) {
                // These are OK
            } else if (localName.equals("li") || localName.equals("RDF") || localName.equals("ID")
                    || localName.equals("about") || localName.equals("parseType") || localName.equals("resource")
                    || localName.equals("nodeID") || localName.equals("datatype")) {
                reportError("<" + qName + "> not allowed as node element");
            } else if (localName.equals("bagID") || localName.equals("aboutEach")
                    || localName.equals("aboutEachPrefix")) {
                reportError(qName + " is no longer a valid RDF name");
            } else {
                reportWarning("unknown rdf element <" + qName + ">");
            }
        }
    }

    /**
     * Checks whether the property element name is from the RDF namespace and, if
     * so, if it is allowed to be used in a property element. If the name is
     * equal to one of the disallowed names (RDF, ID, about, parseType, resource
     * and li), an error is generated. If the name is not defined in the RDF
     * namespace, but it claims that it is from this namespace, a warning is
     * generated.
     */
    private void checkPropertyEltName(String namespaceURI, String localName, String qName)
            throws RDFParseException {
        if (RDF.NAMESPACE.equals(namespaceURI)) {

            if (localName.equals("li") || localName.equals("Seq") || localName.equals("Bag")
                    || localName.equals("Alt") || localName.equals("Statement") || localName.equals("Property")
                    || localName.equals("List") || localName.equals("subject") || localName.equals("predicate")
                    || localName.equals("object") || localName.equals("type") || localName.equals("value")
                    || localName.equals("first") || localName.equals("rest") || localName.equals("nil")
                    || localName.startsWith("_")) {
                // These are OK
            } else if (localName.equals("Description") || localName.equals("RDF") || localName.equals("ID")
                    || localName.equals("about") || localName.equals("parseType") || localName.equals("resource")
                    || localName.equals("nodeID") || localName.equals("datatype")) {
                reportError("<" + qName + "> not allowed as property element");
            } else if (localName.equals("bagID") || localName.equals("aboutEach")
                    || localName.equals("aboutEachPrefix")) {
                reportError(qName + " is no longer a valid RDF name");
            } else {
                reportWarning("unknown rdf element <" + qName + ">");
            }
        }
    }

    /**
     * Checks whether 'atts' contains attributes from the RDF namespace that are
     * not allowed as attributes. If such an attribute is found, an error is
     * generated and the attribute is removed from 'atts'. If the attribute is
     * not defined in the RDF namespace, but it claims that it is from this
     * namespace, a warning is generated.
     */
    private void checkRDFAtts(Atts atts)
            throws RDFParseException {
        Iterator<Att> iter = atts.iterator();

        while (iter.hasNext()) {
            Att att = iter.next();

            if (RDF.NAMESPACE.equals(att.getNamespace())) {
                String localName = att.getLocalName();

                if (localName.equals("Seq") || localName.equals("Bag") || localName.equals("Alt")
                        || localName.equals("Statement") || localName.equals("Property")
                        || localName.equals("List") || localName.equals("subject") || localName.equals("predicate")
                        || localName.equals("object") || localName.equals("type") || localName.equals("value")
                        || localName.equals("first") || localName.equals("rest") || localName.equals("nil")
                        || localName.startsWith("_")) {
                    // These are OK
                } else if (localName.equals("Description") || localName.equals("li") || localName.equals("RDF")
                        || localName.equals("ID") || localName.equals("about") || localName.equals("parseType")
                        || localName.equals("resource") || localName.equals("nodeID")
                        || localName.equals("datatype")) {
                    reportError("'" + att.getQName() + "' not allowed as attribute name");
                    iter.remove();
                } else if (localName.equals("bagID") || localName.equals("aboutEach")
                        || localName.equals("aboutEachPrefix")) {
                    reportError(att.getQName() + " is no longer a valid RDF name");
                } else {
                    reportWarning("unknown rdf attribute '" + att.getQName() + "'");
                }
            }
        }
    }

    /**
     * Checks whether 'atts' is empty. If this is not the case, a warning is
     * generated for each attribute that is still present.
     */
    private void checkNoMoreAtts(Atts atts)
            throws RDFParseException {
        if (atts.size() > 0) {
            Iterator<Att> iter = atts.iterator();

            while (iter.hasNext()) {
                Att att = iter.next();

                reportError("unexpected attribute '" + att.getQName() + "'");
                iter.remove();
            }
        }
    }

    /**
     * Reports a stament to the configured RDFHandlerException.
     *
     * @param subject   The statement's subject.
     * @param predicate The statement's predicate.
     * @param object    The statement's object.
     * @throws RDFHandlerException If the configured RDFHandlerException throws an
     *                             RDFHandlerException.
     */
    private void reportStatement(Resource subject, URI predicate, Value object)
            throws RDFParseException, RDFHandlerException {
        Statement st;
        if(context == null || context.equals("")) {
            st = createStatement(subject, predicate, object);
        } else {
            URI contextURI = null;
            try {
                contextURI = new URIImpl(context);
            } catch (Exception e) {
                contextURI = new URIImpl(documentURI + context);
            }
            st = createStatement(subject, predicate, object, contextURI);
        }

        rdfHandler.handleStatement(st);
    }

    /**
     * Overrides {@link RDFParserBase#reportWarning(String)}, adding line- and
     * column number information to the error.
     */
    protected void reportWarning(String msg) {
        Locator locator = saxFilter.getLocator();
        if (locator != null) {
            reportWarning(msg, locator.getLineNumber(), locator.getColumnNumber());
        } else {
            reportWarning(msg, -1, -1);
        }
    }

    /**
     * Overrides {@link RDFParserBase#reportError(String)}, adding line- and
     * column number information to the error.
     */
    protected void reportError(String msg)
            throws RDFParseException {
        Locator locator = saxFilter.getLocator();
        if (locator != null) {
            reportError(msg, locator.getLineNumber(), locator.getColumnNumber());
        } else {
            reportError(msg, -1, -1);
        }
    }

    /**
     * Overrides {@link RDFParserBase#reportFatalError(String)}, adding line- and
     * column number information to the error.
     */
    protected void reportFatalError(String msg)
            throws RDFParseException {
        Locator locator = saxFilter.getLocator();
        if (locator != null) {
            reportFatalError(msg, locator.getLineNumber(), locator.getColumnNumber());
        } else {
            reportFatalError(msg, -1, -1);
        }
    }

    /**
     * Overrides {@link RDFParserBase#reportFatalError(Exception)}, adding line-
     * and column number information to the error.
     */
    protected void reportFatalError(Exception e)
            throws RDFParseException {
        Locator locator = saxFilter.getLocator();
        if (locator != null) {
            reportFatalError(e, locator.getLineNumber(), locator.getColumnNumber());
        } else {
            reportFatalError(e, -1, -1);
        }
    }

    /*-----------------------------------------------*
      * Inner classes NodeElement and PropertyElement *
      *-----------------------------------------------*/

    static class NodeElement {

        private Resource resource;

        private boolean isVolatile = false;
        ;

        private int liCounter = 1;

        public NodeElement(Resource resource) {
            this.resource = resource;
        }

        public Resource getResource() {
            return resource;
        }

        public void setIsVolatile(boolean isVolatile) {
            this.isVolatile = isVolatile;
        }

        public boolean isVolatile() {
            return isVolatile;
        }

        public int getNextLiCounter() {
            return liCounter++;
        }
    }

    static class PropertyElement {

        /**
         * The property URI.
         */
        private URI uri;

        /**
         * An optional reification identifier.
         */
        private URI reificationURI;

        /**
         * An optional datatype.
         */
        private URI datatype;

        /**
         * Flag indicating whether this PropertyElement has an attribute
         * <tt>rdf:parseType="Collection"</tt>.
         */
        private boolean parseCollection = false;

        /**
         * The resource that was used to append the last part of an rdf:List.
         */
        private Resource lastListResource;

        public PropertyElement(URI uri) {
            this.uri = uri;
        }

        public URI getURI() {
            return uri;
        }

        public boolean isReified() {
            return reificationURI != null;
        }

        public void setReificationURI(URI reifURI) {
            this.reificationURI = reifURI;
        }

        public URI getReificationURI() {
            return reificationURI;
        }

        public void setDatatype(URI datatype) {
            this.datatype = datatype;
        }

        public URI getDatatype() {
            return datatype;
        }

        public boolean parseCollection() {
            return parseCollection;
        }

        public void setParseCollection(boolean parseCollection) {
            this.parseCollection = parseCollection;
        }

        public Resource getLastListResource() {
            return lastListResource;
        }

        public void setLastListResource(Resource resource) {
            lastListResource = resource;
        }
    }

    class Att {

        /*-----------*
         * Variables *
         *-----------*/

        private String namespace;

        private String localName;

        private String qName;

        private String value;

        /*--------------*
         * Constructors *
         *--------------*/

        public Att(String namespace, String localName, String qName, String value) {
            this.namespace = namespace;
            this.localName = localName;
            this.qName = qName;
            this.value = value;
        }

        /*---------*
         * Methods *
         *---------*/

        public String getNamespace() {
            return namespace;
        }

        public String getLocalName() {
            return localName;
        }

        public String getURI() {
            return namespace + localName;
        }

        public String getQName() {
            return qName;
        }

        public String getValue() {
            return value;
        }

    }

    class Atts {

        /*-----------*
         * Variables *
         *-----------*/

        /**
         * List containing Att objects.
         */
        private List<Att> attributes;

        /*--------------*
         * Constructors *
         *--------------*/

        /**
         * Creates a new <tt>Atts</tt> object.
         */
        public Atts() {
            this(4);
        }

        /**
         * Creates a new <tt>Atts</tt> object.
         *
         * @param size The initial size of the array for storing attributes.
         */
        public Atts(int size) {
            attributes = new ArrayList<Att>(size);
        }

        /*---------*
         * Methods *
         *---------*/

        /**
         * Adds an attribute.
         */
        public void addAtt(Att att) {
            attributes.add(att);
        }

        /**
         * Get an iterator on the attributes.
         *
         * @return an Iterator over Att objects.
         */
        public Iterator<Att> iterator() {
            return attributes.iterator();
        }

        /**
         * Gets the attribute with the specified QName.
         *
         * @param qName The QName of an attribute.
         * @return The attribute with the specified QName, or <tt>null</tt> if no
         *         such attribute could be found.
         */
        public Att getAtt(String qName) {
            for (int i = 0; i < attributes.size(); i++) {
                Att att = attributes.get(i);

                if (att.getQName().equals(qName)) {
                    return att;
                }
            }

            return null;
        }

        /**
         * Gets the attribute with the specified namespace and local name.
         *
         * @param namespace The namespace of an attribute.
         * @param localName The local name of an attribute.
         * @return The attribute with the specified namespace and local name, or
         *         <tt>null</tt> if no such attribute could be found.
         */
        public Att getAtt(String namespace, String localName) {
            for (int i = 0; i < attributes.size(); i++) {
                Att att = attributes.get(i);

                if (att.getLocalName().equals(localName) && att.getNamespace().equals(namespace)) {
                    return att;
                }
            }

            return null;
        }

        /**
         * Removes the attribute with the specified QName and returns it.
         *
         * @param qName The QName of an attribute.
         * @return The removed attribute, or <tt>null</tt> if no attribute with the
         *         specified QName could be found.
         */
        public Att removeAtt(String qName) {
            for (int i = 0; i < attributes.size(); i++) {
                Att att = attributes.get(i);

                if (att.getQName().equals(qName)) {
                    attributes.remove(i);
                    return att;
                }
            }

            return null;
        }

        /**
         * Removes the attribute with the specified namespace and local name and
         * returns it.
         *
         * @param namespace The namespace of an attribute.
         * @param localName The local name of an attribute.
         * @return The removed attribute, or <tt>null</tt> if no attribute with the
         *         specified namespace and local name could be found.
         */
        public Att removeAtt(String namespace, String localName) {
            for (int i = 0; i < attributes.size(); i++) {
                Att att = attributes.get(i);

                if (att.getLocalName().equals(localName) && att.getNamespace().equals(namespace)) {
                    attributes.remove(i);
                    return att;
                }
            }

            return null;
        }

        /**
         * Returns the number of attributes contained in this object.
         */
        public int size() {
            return attributes.size();
        }

        /**
         * Produces a String-representation of this object.
         */
        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append("Atts[");
            for (int i = 0; i < attributes.size(); i++) {
                Att att = attributes.get(i);
                sb.append(att.getQName());
                sb.append("=");
                sb.append(att.getValue());
                sb.append("; ");
            }
            sb.append("]");
            return sb.toString();
        }
    }

    /**
     * A filter on SAX events to make life easier on the RDF parser itself. This
     * filter does things like combining a call to startElement() that is directly
     * followed by a call to endElement() to a single call to emptyElement().
     */
    class SAXFilter implements ContentHandler {

        /*-----------*
         * Variables *
         *-----------*/

        /**
         * The RDF parser to supply the filtered SAX events to.
         */
        private RdfQuadsReader rdfParser;

        /**
         * A Locator indicating a position in the text that is currently being parsed
         * by the SAX parser.
         */
        private Locator locator;

        /**
         * Stack of ElementInfo objects.
         */
        private Stack<ElementInfo> elInfoStack = new Stack<ElementInfo>();

        /**
         * StringBuilder used to collect text during parsing.
         */
        private StringBuilder charBuf = new StringBuilder(512);

        /**
         * The document's URI.
         */
        private ParsedURI documentURI;

        /**
         * Flag indicating whether the parser parses stand-alone RDF documents. In
         * stand-alone documents, the rdf:RDF element is optional if it contains just
         * one element.
         */
        private boolean parseStandAloneDocuments = true;

        /**
         * Variable used to defer reporting of start tags. Reporting start tags is
         * deferred to be able to combine a start tag and an immediately following
         * end tag to a single call to emptyElement().
         */
        private ElementInfo deferredElement = null;

        /**
         * New namespace mappings that have been reported for the next start tag by
         * the SAX parser, but that are not yet assigned to an ElementInfo object.
         */
        private Map<String, String> newNamespaceMappings = new LinkedHashMap<String, String>();

        /**
         * Flag indicating whether we're currently parsing RDF elements.
         */
        private boolean inRDFContext;

        /**
         * The number of elements on the stack that are in the RDF context.
         */
        private int rdfContextStackHeight;

        /**
         * Flag indicating whether we're currently parsing an XML literal.
         */
        private boolean parseLiteralMode = false;

        /**
         * The number of elements on the stack that are part of an XML literal.
         */
        private int xmlLiteralStackHeight;

        /**
         * The prefixes that are defined in the XML literal itself (this in contrast
         * to the namespaces from the XML literal's context).
         */
        private List<String> xmlLiteralPrefixes = new ArrayList<String>();

        /**
         * The prefixes that were used in an XML literal, but that were not defined
         * in it (but rather in the XML literal's context).
         */
        private List<String> unknownPrefixesInXMLLiteral = new ArrayList<String>();

        /*--------------*
         * Constructors *
         *--------------*/

        public SAXFilter(RdfQuadsReader rdfParser) {
            this.rdfParser = rdfParser;
        }

        /*---------*
         * Methods *
         *---------*/

        public Locator getLocator() {
            return locator;
        }

        public void clear() {
            locator = null;
            elInfoStack.clear();
            charBuf.setLength(0);
            documentURI = null;
            deferredElement = null;

            newNamespaceMappings.clear();

            inRDFContext = false;
            rdfContextStackHeight = 0;

            parseLiteralMode = false;
            xmlLiteralStackHeight = 0;

            xmlLiteralPrefixes.clear();
            unknownPrefixesInXMLLiteral.clear();
        }

        public void setDocumentURI(String documentURI) {
            this.documentURI = createBaseURI(documentURI);
        }

        public void setParseStandAloneDocuments(boolean standAloneDocs) {
            parseStandAloneDocuments = standAloneDocs;
        }

        public boolean getParseStandAloneDocuments() {
            return parseStandAloneDocuments;
        }

        /*---------------------------------------*
         * Methods from interface ContentHandler *
         *---------------------------------------*/

        public void setDocumentLocator(Locator loc) {
            locator = loc;

            ParseLocationListener pll = rdfParser.getParseLocationListener();
            if (pll != null) {
                pll.parseLocationUpdate(loc.getLineNumber(), loc.getColumnNumber());
            }
        }

        public void startDocument() {
            // ignore
        }

        public void endDocument() {
            // ignore
        }

        public void startPrefixMapping(String prefix, String uri)
                throws SAXException {
            try {
                if (deferredElement != null) {
                    // This new prefix mapping must come from a new start tag
                    reportDeferredStartElement();
                }

                newNamespaceMappings.put(prefix, uri);

                if (parseLiteralMode) {
                    // This namespace is introduced inside an XML literal
                    xmlLiteralPrefixes.add(prefix);
                }

                rdfParser.getRDFHandler().handleNamespace(prefix, uri);
            } catch (RDFParseException e) {
                throw new SAXException(e);
            } catch (RDFHandlerException e) {
                throw new SAXException(e);
            }
        }

        public void endPrefixMapping(String prefix) {
            if (parseLiteralMode) {
                xmlLiteralPrefixes.remove(prefix);
            }
        }

        public void startElement(String namespaceURI, String localName, String qName, Attributes attributes)
                throws SAXException {
            try {
                if (deferredElement != null) {
                    // The next call could set parseLiteralMode to true!
                    reportDeferredStartElement();
                }

                if (parseLiteralMode) {
                    appendStartTag(qName, attributes);
                    xmlLiteralStackHeight++;
                } else {
                    ElementInfo parent = peekStack();
                    ElementInfo elInfo = new ElementInfo(parent, qName, namespaceURI, localName);

                    elInfo.setNamespaceMappings(newNamespaceMappings);
                    newNamespaceMappings.clear();

                    if (!inRDFContext && parseStandAloneDocuments
                            && (!localName.equals("RDF") || !namespaceURI.equals(RDF.NAMESPACE))) {
                        // Stand-alone document that does not start with an rdf:RDF root
                        // element. Assume this root element is omitted.
                        inRDFContext = true;
                    }

                    if (!inRDFContext) {
                        // Check for presence of xml:base and xml:lang attributes.
                        for (int i = 0; i < attributes.getLength(); i++) {
                            String attQName = attributes.getQName(i);


                            if ("xml:base".equals(attQName)) {
                                elInfo.setBaseURI(attributes.getValue(i));
                            } else if ("xml:lang".equals(attQName)) {
                                elInfo.xmlLang = attributes.getValue(i);
                            } else if (attributes.getLocalName(i).equals("graph") &&
                                    attributes.getURI(i).equals(RdfQuadsReader.GRAPH_EXTENSION_NAMESPACE)) {
                                elInfo.context = attributes.getValue(i);
                            }
                        }

                        elInfoStack.push(elInfo);

                        // Check if we are entering RDF context now.
                        if (localName.equals("RDF") && namespaceURI.equals(RDF.NAMESPACE)) {
                            inRDFContext = true;
                            rdfContextStackHeight = 0;
                        }
                    } else {
                        // We're parsing RDF elements.
                        checkAndCopyAttributes(attributes, elInfo);

                        // Don't report the new element to the RDF parser just yet.
                        deferredElement = elInfo;
                    }

                    charBuf.setLength(0);
                }
            } catch (RDFParseException e) {
                throw new SAXException(e);
            } catch (RDFHandlerException e) {
                throw new SAXException(e);
            }
        }

        private void reportDeferredStartElement()
                throws RDFParseException, RDFHandlerException {

            // Only useful for debugging.
            // if (deferredElement == null) {
            // throw new RuntimeException("no deferred start element available");
            // }

            elInfoStack.push(deferredElement);
            rdfContextStackHeight++;

            rdfParser.setBaseURI(deferredElement.baseURI);
            rdfParser.setXMLLang(deferredElement.xmlLang);
            rdfParser.setContext(deferredElement.context);

            rdfParser.startElement(deferredElement.namespaceURI, deferredElement.localName, deferredElement.qName,
                    deferredElement.atts);

            deferredElement = null;
        }

        public void endElement(String namespaceURI, String localName, String qName)
                throws SAXException {
            try {
                // FIXME: in parseLiteralMode we should also check if start- and
                // end-tags match but these start tags are not tracked yet.

                if (rdfParser.verifyData() && !parseLiteralMode) {
                    // Verify that the end tag matches the start tag.
                    ElementInfo elInfo;

                    if (deferredElement != null) {
                        elInfo = deferredElement;
                    } else {
                        elInfo = peekStack();
                    }

                    if (!qName.equals(elInfo.qName)) {
                        rdfParser.reportFatalError("expected end tag </'" + elInfo.qName + ">, found </" + qName + ">");
                    }
                }

                if (!inRDFContext) {
                    elInfoStack.pop();
                    charBuf.setLength(0);
                    return;
                }

                if (deferredElement == null && rdfContextStackHeight == 0) {
                    // This end tag removes the element that signaled the start
                    // of the RDF context (i.e. <rdf:RDF>) from the stack.
                    inRDFContext = false;

                    elInfoStack.pop();
                    charBuf.setLength(0);
                    return;
                }

                // We're still in RDF context.

                if (parseLiteralMode && xmlLiteralStackHeight > 0) {
                    appendEndTag(qName);
                    xmlLiteralStackHeight--;
                    return;
                }

                // Check for any deferred start elements
                if (deferredElement != null) {
                    // Start element still deferred, this is an empty element
                    rdfParser.setBaseURI(deferredElement.baseURI);
                    rdfParser.setXMLLang(deferredElement.xmlLang);
                    rdfParser.setContext(deferredElement.context);

                    rdfParser.emptyElement(deferredElement.namespaceURI, deferredElement.localName,
                            deferredElement.qName, deferredElement.atts);

                    deferredElement = null;
                } else {
                    if (parseLiteralMode) {
                        // Insert any used namespace prefixes from the XML literal's
                        // context that are not defined in the XML literal itself.
                        insertUsedContextPrefixes();

                        rdfParser.text(charBuf.toString());

                        parseLiteralMode = false;
                    } else {
                        String s = charBuf.toString().trim();

                        if (s.length() > 0) {
                            rdfParser.text(s);
                        }
                    }

                    charBuf.setLength(0);

                    // Handle the end tag
                    elInfoStack.pop();
                    rdfContextStackHeight--;

                    rdfParser.endElement(namespaceURI, localName, qName);
                }
            } catch (RDFParseException e) {
                throw new SAXException(e);
            } catch (RDFHandlerException e) {
                throw new SAXException(e);
            }
        }

        public void characters(char[] ch, int start, int length)
                throws SAXException {
            try {
                if (inRDFContext) {
                    if (deferredElement != null) {
                        reportDeferredStartElement();
                    }

                    if (parseLiteralMode) {
                        // Characters like '<', '>', and '&' must be escaped to
                        // prevent breaking the XML text.
                        String s = new String(ch, start, length);
                        s = XMLUtil.escapeCharacterData(s);
                        charBuf.append(s);
                    } else {
                        charBuf.append(ch, start, length);
                    }
                }
            } catch (RDFParseException e) {
                throw new SAXException(e);
            } catch (RDFHandlerException e) {
                throw new SAXException(e);
            }
        }

        public void ignorableWhitespace(char[] ch, int start, int length) {
            if (parseLiteralMode) {
                charBuf.append(ch, start, length);
            }
        }

        public void processingInstruction(String target, String data) {
            // ignore
        }

        public void skippedEntity(String name) {
            // ignore
        }

        private void checkAndCopyAttributes(Attributes attributes, ElementInfo elInfo)
                throws SAXException, RDFParseException {
            Atts atts = new Atts(attributes.getLength());

            int attCount = attributes.getLength();
            for (int i = 0; i < attCount; i++) {
                String qName = attributes.getQName(i);
                String value = attributes.getValue(i);

                // attributes starting with "xml" should be ignored, except for the
                // ones that are handled by this parser (xml:lang and xml:base).
                if (qName.startsWith("xml")) {
                    if (qName.equals("xml:lang")) {
                        elInfo.xmlLang = value;
                    } else if (qName.equals("xml:base")) {
                        elInfo.setBaseURI(value);
                    }
                } else if(attributes.getURI(i).equals(GRAPH_EXTENSION_NAMESPACE) &&
                        attributes.getLocalName(i).equals("graph")){
                    elInfo.context = value;
                } else {
                    String namespace = attributes.getURI(i);
                    String localName = attributes.getLocalName(i);

                    // A limited set of unqualified attributes must be supported by
                    // parsers, as is specified in section 6.1.4 of the spec
                    if ("".equals(namespace)) {
                        if (localName.equals("ID") || localName.equals("about") || localName.equals("resource")
                                || localName.equals("parseType") || localName.equals("type")) {
                            rdfParser.reportWarning("use of unqualified attribute " + localName
                                    + " has been deprecated");
                            namespace = RDF.NAMESPACE;
                        }
                    }

                    if (rdfParser.verifyData()) {
                        if ("".equals(namespace)) {
                            rdfParser.reportError("unqualified attribute '" + qName + "' not allowed");
                        }
                    }

                    Att att = new Att(namespace, localName, qName, value);
                    atts.addAtt(att);
                }
            }

            elInfo.atts = atts;
        }

        public void setParseLiteralMode() {
            parseLiteralMode = true;
            xmlLiteralStackHeight = 0;

            // All currently known namespace prefixes are
            // new for this XML literal.
            xmlLiteralPrefixes.clear();
            unknownPrefixesInXMLLiteral.clear();
        }

        private ParsedURI createBaseURI(String uriString) {
            ParsedURI uri = new ParsedURI(uriString);
            uri.normalize();
            return uri;
        }

        /*---------------------------------*
         * Methods related to XML literals *
         *---------------------------------*/

        /**
         * Appends a start tag to charBuf. This method is used during the parsing of
         * an XML Literal.
         */
        private void appendStartTag(String qName, Attributes attributes) {
            // Write start of start tag
            charBuf.append("<" + qName);

            // Write any new namespace prefix definitions
            for (Map.Entry<String, String> entry : newNamespaceMappings.entrySet()) {
                String prefix = entry.getKey();
                String namespace = entry.getValue();
                appendNamespaceDecl(charBuf, prefix, namespace);
            }

            // Write attributes
            int attCount = attributes.getLength();
            for (int i = 0; i < attCount; i++) {
                appendAttribute(charBuf, attributes.getQName(i), attributes.getValue(i));
            }

            // Write end of start tag
            charBuf.append(">");

            // Check for any used prefixes that are not
            // defined in the XML literal itself
            int colonIdx = qName.indexOf(':');
            String prefix = (colonIdx > 0) ? qName.substring(0, colonIdx) : "";

            if (!xmlLiteralPrefixes.contains(prefix) && !unknownPrefixesInXMLLiteral.contains(prefix)) {
                unknownPrefixesInXMLLiteral.add(prefix);
            }
        }

        /**
         * Appends an end tag to charBuf. This method is used during the parsing of
         * an XML Literal.
         */
        private void appendEndTag(String qName) {
            charBuf.append("</" + qName + ">");
        }

        /**
         * Inserts prefix mappings from an XML Literal's context for all prefixes
         * that are used in the XML Literal and that are not defined in the XML
         * Literal itself.
         */
        private void insertUsedContextPrefixes() {
            int unknownPrefixesCount = unknownPrefixesInXMLLiteral.size();

            if (unknownPrefixesCount > 0) {
                // Create a String with all needed context prefixes
                StringBuilder contextPrefixes = new StringBuilder(1024);
                ElementInfo topElement = peekStack();

                for (int i = 0; i < unknownPrefixesCount; i++) {
                    String prefix = unknownPrefixesInXMLLiteral.get(i);
                    String namespace = topElement.getNamespace(prefix);
                    if (namespace != null) {
                        appendNamespaceDecl(contextPrefixes, prefix, namespace);
                    }
                }

                // Insert this String before the first '>' character
                int endOfFirstStartTag = charBuf.indexOf(">");
                charBuf.insert(endOfFirstStartTag, contextPrefixes.toString());
            }

            unknownPrefixesInXMLLiteral.clear();
        }

        private void appendNamespaceDecl(StringBuilder sb, String prefix, String namespace) {
            String attName = "xmlns";

            if (!"".equals(prefix)) {
                attName += ":" + prefix;
            }

            appendAttribute(sb, attName, namespace);
        }

        private void appendAttribute(StringBuilder sb, String name, String value) {
            sb.append(" ");
            sb.append(name);
            sb.append("=\"");
            sb.append(XMLUtil.escapeDoubleQuotedAttValue(value));
            sb.append("\"");
        }

        /*------------------------------------------*
         * Methods related to the ElementInfo stack *
         *------------------------------------------*/

        private ElementInfo peekStack() {
            ElementInfo result = null;

            if (!elInfoStack.empty()) {
                result = elInfoStack.peek();
            }

            return result;
        }

        /*----------------------------*
         * Internal class ElementInfo *
         *----------------------------*/

        private class ElementInfo {

            public String qName;

            public String namespaceURI;

            public String localName;

            public Atts atts;

            public ElementInfo parent;

            private Map<String, String> namespaceMap;

            public ParsedURI baseURI;

            public String xmlLang;

            public String context;

            public ElementInfo(String qName, String namespaceURI, String localName) {
                this(null, qName, namespaceURI, localName);
            }

            public ElementInfo(ElementInfo parent, String qName, String namespaceURI, String localName) {
                this.parent = parent;
                this.qName = qName;
                this.namespaceURI = namespaceURI;
                this.localName = localName;

                if (parent != null) {
                    // Inherit baseURI, context and xmlLang from parent
                    this.baseURI = parent.baseURI;
                    this.xmlLang = parent.xmlLang;
                    this.context = parent.context;
                } else {
                    this.baseURI = documentURI;
                    this.xmlLang = "";
                    this.context = "";
                }
            }

            public void setBaseURI(String uriString) {
                // Resolve the specified base URI against the inherited base URI
                baseURI = baseURI.resolve(createBaseURI(uriString));
            }

            public void setNamespaceMappings(Map<String, String> namespaceMappings) {
                if (namespaceMappings.isEmpty()) {
                    namespaceMap = null;
                } else {
                    namespaceMap = new HashMap<String, String>(namespaceMappings);
                }
            }

            public String getNamespace(String prefix) {
                String result = null;

                if (namespaceMap != null) {
                    result = namespaceMap.get(prefix);
                }

                if (result == null && parent != null) {
                    result = parent.getNamespace(prefix);
                }

                return result;
            }
        }
    }


}
